perf(evm): reuse EVMFrame objects to avoid 32 KB zero-init per call#418
perf(evm): reuse EVMFrame objects to avoid 32 KB zero-init per call#418starwarfan wants to merge 2 commits intoDTVMStack:mainfrom
Conversation
EVMFrame contains a std::array<uint256, 1024> (32 KB) that was being zero-initialized on every allocTopFrame() via vector::emplace_back(). Instead of clearing and re-constructing frames, track active frame count separately and reuse previously allocated EVMFrame objects, resetting only the necessary scalar fields (Sp, Pc, Host, etc.). This eliminates ~1us of memset overhead per EVM execution, yielding a ~16% improvement on ERC-20 transfer benchmarks measured via evmc_tool. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
⚡ Performance Regression Check Results✅ Performance Check Passed (interpreter)Performance Benchmark Results (threshold: 25%)
Summary: 194 benchmarks, 0 regressions ✅ Performance Check Passed (multipass)Performance Benchmark Results (threshold: 25%)
Summary: 194 benchmarks, 0 regressions |
There was a problem hiding this comment.
Pull request overview
This PR optimizes the EVM interpreter’s call-frame management by reusing previously allocated EVMFrame objects instead of reconstructing them per call, avoiding repeated 32KB stack-array zero-initialization and reducing per-execution overhead.
Changes:
- Add
FrameCountto track the logically active portion ofFrameStackwhile keeping oldEVMFrameobjects alive for reuse. - Update
allocTopFrame()/freeBackFrame()andgetCurFrame()to useFrameCountand selectively reset scalar fields on reuse.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/evm/interpreter.h |
Adds FrameCount, updates reset/getCurFrame semantics and documentation to support frame reuse. |
src/evm/interpreter.cpp |
Reworks frame allocation/freeing to reuse EVMFrame objects and reset only key fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/evm/interpreter.cpp
Outdated
| EVMFrame &Frame = FrameStack[FrameCount]; | ||
| Frame.Sp = 0; | ||
| Frame.Pc = 0; | ||
| Frame.Host = nullptr; | ||
| Frame.Memory.clear(); // keeps capacity | ||
| Frame.CallData.clear(); // keeps capacity | ||
| Frame.MTx = {}; | ||
| Frame.Value = 0; | ||
| } else { |
There was a problem hiding this comment.
Reusing EVMFrame objects now retains per-frame Memory and CallData capacities across calls (clear() keeps capacity). Previously FrameStack.clear() destroyed frames and released these buffers. This can significantly increase steady-state RSS after a single call that expands memory a lot or has large calldata, and also keeps prior calldata contents resident longer than before. Consider adding a cap (e.g., if capacity exceeds a threshold, release/shrink) or explicitly releasing these vectors during resetForNewCall() while still reusing the fixed-size Stack array.
src/evm/interpreter.cpp
Outdated
| if (FrameCount < FrameStack.size()) { | ||
| // Reuse an existing EVMFrame object – avoids zero-initializing the | ||
| // 32 KB uint256 stack array. Only reset the fields that matter. | ||
| EVMFrame &Frame = FrameStack[FrameCount]; | ||
| Frame.Sp = 0; | ||
| Frame.Pc = 0; | ||
| Frame.Host = nullptr; | ||
| Frame.Memory.clear(); // keeps capacity | ||
| Frame.CallData.clear(); // keeps capacity | ||
| Frame.MTx = {}; | ||
| Frame.Value = 0; | ||
| } else { | ||
| FrameStack.emplace_back(); | ||
| } | ||
| EVMFrame &Frame = FrameStack[FrameCount]; | ||
| ++FrameCount; |
There was a problem hiding this comment.
allocTopFrame() declares EVMFrame &Frame inside the reuse branch and then immediately declares another EVMFrame &Frame after the if/else. This shadowing makes it easier to accidentally reset fields on one reference and use the other later. Consider using a single reference/pointer initialized once (and then conditionally resetting) to avoid shadowing.
src/evm/interpreter.h
Outdated
| /// Reset state for reuse across calls. Keeps allocated EVMFrame objects | ||
| /// (and their 32 KB stack arrays) alive so that the next allocTopFrame() | ||
| /// only needs to reset a few scalar fields instead of zero-initializing | ||
| /// the entire array. | ||
| void resetForNewCall(runtime::EVMInstance *NewInst) { | ||
| Inst = NewInst; | ||
| FrameStack.clear(); // keeps vector capacity | ||
| FrameCount = 0; // logically empty, but frames stay allocated | ||
| Status = EVMC_SUCCESS; |
There was a problem hiding this comment.
The updated resetForNewCall() doc comment mentions keeping EVMFrame objects/stack arrays alive, but with the new reuse logic the per-frame Memory and CallData vector capacities are also kept across calls. It would help to mention this explicitly (or document that only sizes are cleared on reuse) so callers understand the memory-retention tradeoff.
EVMFrame contains a std::array<uint256, 1024> (32 KB) that was being zero-initialized on every allocTopFrame() via vector::emplace_back(). Instead of clearing and re-constructing frames, track active frame count separately and reuse previously allocated EVMFrame objects, resetting only the necessary scalar fields (Sp, Pc, Host, etc.).
This eliminates ~1us of memset overhead per EVM execution, yielding a ~16% improvement on ERC-20 transfer benchmarks measured via evmc_tool.
1. Does this PR affect any open issues?(Y/N) and add issue references (e.g. "fix #123", "re #123".):
2. What is the scope of this PR (e.g. component or file name):
3. Provide a description of the PR(e.g. more details, effects, motivations or doc link):
4. Are there any breaking changes?(Y/N) and describe the breaking changes(e.g. more details, motivations or doc link):
5. Are there test cases for these changes?(Y/N) select and add more details, references or doc links:
6. Release note